package com.tabbinder.server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.Transaction;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.users.User;
import com.tabbinder.client.Constants;

public class BinderServlet extends CommonServlet {
	private static Logger log = Logger.getLogger(BinderServlet.class.getName());

	@SuppressWarnings("unchecked")
	@Override
	public void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws IOException {

		User user = getUser(resp);
		if (user == null) {
			// response already filled in
			return;
		}

		String id = req.getParameter("id");

		PrintWriter out = resp.getWriter();
		PersistenceManager pm = PMF.get().getPersistenceManager();
		JSONArray list = new JSONArray();
		List<BindRecord> records;
		if (id == null) {
			Query query = pm.newQuery("select from "
					+ BindRecord.class.getName() + " where user == userParam "
					+ "parameters " + User.class.getName() + " userParam");
			records = (List<BindRecord>) query.execute(user);
		} else {
			Query query = pm.newQuery("select from "
					+ BindRecord.class.getName()
					+ " where user == userParam && binder == id "
					+ "parameters " + User.class.getName() + " userParam, "
					+ Long.class.getName() + " id");
			records = (List<BindRecord>) query.execute(user, new Long(id));
		}
		log.log(Level.INFO, "Got {0} records for {1}", new Object[] {
				records.size(), user });

		for (BindRecord r : records) {
			list.put(makeDetailJson(r));
		}
		pm.close();
		out.append(list.toString());
	}

	@Override
	public void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws IOException {
		
		// For some reason DELETE does not work on the server. This is a hack
		// around it
		String action = req.getParameter("action");
		if ("delete".equals(action)) {
			doDelete(req, resp);
			return;
		}
		
		PrintWriter out = resp.getWriter();

		User user = getUser(resp);
		if (user == null) {
			// response already filled in
			return;
		}

		// Get the json object out of the request
		BufferedReader reader = req.getReader();
		StringBuffer input = new StringBuffer();
		String str = reader.readLine();
		while (str != null) {
			input.append(str);
			str = reader.readLine();
		}

		try {
			JSONObject item = new JSONObject(new JSONTokener(input.toString()));
			Key recordId = null;
			// New objects come in with id == -1
			if (item.getLong("id") == -1)
				recordId = processNew(item, user);
			else
				recordId = processUpdate(item, user);

			// Send the new record in detail
			if (recordId == null)
				throw new IOException("Could not create binder");

			PersistenceManager pm = PMF.get().getPersistenceManager();
			BindRecord r = pm.getObjectById(BindRecord.class, recordId);
			if (r == null)
				throw new IOException("Newly created record gone");
			JSONObject res = makeDetailJson(r);
			out.append(res.toString());
		} catch (Exception e) {
			resp.setStatus(Constants.STATUS_CODE_BAD_GATEWAY);
			out.append("Error: " + e);
			log.log(Level.SEVERE, e.getMessage());
		}
	}

	@Override
	public void doDelete(HttpServletRequest req, HttpServletResponse resp)
			throws IOException {

		User user = getUser(resp);
		if (user == null) {
			// response already filled in
			return;
		}

		// Get the id. Should be just one number
		String str = req.getReader().readLine();
		Long id = new Long(str);

		PrintWriter out = resp.getWriter();

		// Find the binder
		PersistenceManager pm = PMF.get().getPersistenceManager();
		Transaction tx = pm.currentTransaction();
		try {
			tx.begin();
			Binder binder = pm.getObjectById(Binder.class, id);
			if (binder == null) {
				resp.setStatus(Constants.STATUS_CODE_BAD_GATEWAY);
				out.append("Binder " + id + " does not exist");
				log.log(Level.SEVERE,
						"Attempt to delete nonexistent binder {0}", id);
				return;
			}

			// Find our record for it
			BindRecord rec = findRecord(pm, id, user);

			// Remove the record
			pm.deletePersistent(rec);

			// If the last record, remove the binder as well
			if (binder.getRecords().size() == 1)
				pm.deletePersistent(binder);
			else
				binder.getRecords().remove(rec);

			tx.commit();
		} catch (Exception ex) {
			log.log(Level.SEVERE, "Cannot delete: ", ex);
			if (tx.isActive())
				tx.rollback();
		}
		pm.close();
	}

	/** Create nice detailed object for the binder/record */
	private JSONObject makeDetailJson(BindRecord rec) {
		JSONObject res = rec.getBinder().toJson();
		rec.addFields(res);
		return res;
	}

	/**
	 * Create and persist new Binder and hook it up to the current user
	 * 
	 * @return id of the bind record, or NULL if error
	 */
	private Key processNew(JSONObject item, User user) {
		PersistenceManager pm = PMF.get().getPersistenceManager();
		Transaction tx = pm.currentTransaction();
		Key recordId;
		try {
			tx.begin();

			// Create brand new Binder from the json object
			Binder binder = new Binder(item);

			// Create a binding record
			BindRecord rec = new BindRecord(binder, user);
			rec.setLastTouched(new Date()); // just touched
			try {
				boolean fav = item.getBoolean("favorite");
				rec.setFavorite(fav);
			} catch (JSONException ex) {
				rec.setFavorite(false);
			}

			pm.makePersistent(binder);
			pm.makePersistent(rec);

			// Remember record id for return
			recordId = rec.getId();

			// Commit all
			tx.commit();
		} catch (Exception e) {
			// Should we send this message to the user? Would it expose too much
			// info about the guts? It's opensource anyway
			log.log(Level.SEVERE, "Cannot create new binder: ", e);
			tx.rollback();
			return null;
		} finally {
			pm.close();
		}
		return recordId;
	}

	/**
	 * Update existing binder
	 * 
	 * @return id of the bind record, or NULL if error
	 */
	private Key processUpdate(JSONObject item, User user) {
		PersistenceManager pm = PMF.get().getPersistenceManager();

		// Binder modifications - separate transaction from the record
		Binder binder = null;
		try {
			Long bid = item.getLong("id");
			if (bid == null)
				throw new IOException("No binder id provided");
			binder = pm.getObjectById(Binder.class, bid);
			if (binder == null)
				throw new IOException("Binder " + bid + " not found");

			Transaction tx = pm.currentTransaction();
			tx.begin();

			// Update fields
			String title = item.getString("title");
			if (title == null)
				title = "";
			binder.setTitle(title);
			binder.setLinksFrom(item.getJSONArray("links"));

			// Commit all
			tx.commit();
		} catch (Exception e) {
			// Should we send this message to the user? Would it expose too much
			// info about the guts? It's opensource anyway
			log.log(Level.SEVERE, "Cannot update binder: ", e);
			return null;
		} finally {
			pm.close();
		}

		// Record modifications
		Key recordId = null; // will return this
		try {
			pm = PMF.get().getPersistenceManager();
			Transaction tx = pm.currentTransaction();
			tx.begin();

			// Find existing bind record by binder and user
			BindRecord rec = findRecord(pm, binder.getId(), user);
			boolean fav = item.getBoolean("favorite");
			rec.setFavorite(fav);
			// Remember record id for return
			recordId = rec.getId();

			// Commit all
			tx.commit();
		} catch (Exception e) {
			// Should we send this message to the user? Would it expose too much
			// info about the guts? It's opensource anyway
			log.log(Level.SEVERE, "Cannot update binder: ", e);
			return null;
		} finally {
			pm.close();
		}
		return recordId;
	}

	@SuppressWarnings("unchecked")
	private BindRecord findRecord(PersistenceManager pm, Long binderId,
			User user) throws IOException {
		Query query = pm.newQuery("select from " + BindRecord.class.getName()
				+ " where user == userParam && binder == binderParam"
				+ " parameters " + User.class.getName() + " userParam, "
				+ Binder.class.getName() + " binderParam");
		HashMap<String, Object> map = new HashMap<String, Object>();
		map.put("userParam", user);
		map.put("binderParam", binderId);
		List<BindRecord> records = (List<BindRecord>) query.executeWithMap(map);
		if (records.isEmpty())
			throw new IOException("No records found");
		if (records.size() > 1)
			throw new IOException("Expected 1 record, got " + records.size());
		return records.get(0);
	}
}
